# 第8章 打包优化

软件工程领域的经验--不要过早优化,在项目初期不要看到任何优化点就拿来加到项目中,这样不但增加了复杂度,优化的效果也不会太理想。一般是当项目发展到一定规模后,性能问题随之而来,这时再去分析然后对症下药,才有可能达到理想的优化效果。

# HappyPack

HappyPack是一个通过多线程来提升Webpack打包速度的工具。

# 工作原理

使用loader将各种资源进行转译处理是非常耗时的。包括使用babel-loader转译ES6+和ts-loader转译TypeScript。

  1. 从配置中获取打包入口。
  2. 匹配loader规则,并对入口模块进行转译。
  3. 对转译后的模块进行依赖查找。
  4. 对新找到的模块重复进行2和3,直到没有新的依赖模块。

webpack是单线程的,如果一个模块依赖几个其他模块,却必须串行执行。HappyPack可以开启多个线程,并行对不同模块进行转译。这样就可以本地计算资源提高打包速度。

# 单个loader的优化

要用HappyPack提供的loader来替换原有的loader,并将原有的那个通过HappyPack插件传进去。

const HappyPack = require("happypack");
module.exports = {
    module: {
        rule: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                loader: 'happpack/loader',
            }
        ]
    },
    plugins: [
        new HappyPack({
            loaders: [
                {
                    loader: 'babel-loader',
                    options: {
                        presets: ['react']
                    }
                }
            ]
        })
    ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 多个loader的优化

const HappyPack = require("happypack");
module.exports = {
    module: {
        rule: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                loader: 'happpack/loader?id=js',
            },
            {
                test: /\.ts$/,
                exclude: /node_modules/,
                loader: 'happypack/loader?id=ts'
            }
        ]
    },
    plugins: [
        new HappyPack({
            id: 'js',
            loaders: [
                {
                    loader: 'babel-loader',
                    options: {
                        presets: ['react']
                    }
                }
            ]
        }),
        new HappyPack({
            id: 'ts',
            loaders: [
                {
                    loader: 'ts-loader',
                    options: {}// ts options
                }
            ]
        })
    ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

# 缩小打包作用域

提升性能的方法:增加资源或者缩小范围。增加资源就是指使用更多CPU和内存,用更多的计算能力来缩短执行任务的时间;缩小范围是针对任务本身,比如去掉冗余的流程,尽量不做重复性的工作。

# exclude 和 indlude

exclude和include在配置loader时一般都会加上它们。对于JS来说,一般要把node_modules目录排除,exclude优先级更高。

# noParse

有些库希望webpack完全不要去解析的,库内部也不会有对其他模块的依赖,那么可以使用noParse对其进行忽略。

module: {
    noParse: /lodash/
}
1
2
3

会忽略多有文件名中包含lodash的模块,这些模块仍然会被打包进资源文件,只不多webpack不会对其进行任何解析。

webpack3及之后还支持完整的路径匹配。

module.exports = {
    module: {
        noParse: function(fullpath) {
            // fullpath是绝对路径,如:/Users/me/app/webpack-no-parse/lib/lodash.js
            return /lib/.test(fullpath);
        }
    }
}
1
2
3
4
5
6
7
8

# IgnorePlugin

它可以完全排除一些模块,被排除的模块即便被引用了也不会被打包进资源文件中。比如Moment.js是一个日期时间处理相关的库,为了做本地化会加载很多语言包,对于我们其他地区的语言包用不到,就可以用IgnorePlugin来去掉。

plugins: [
    new webpack.IgnorePlugin({
        resourceRegExp: /^\.\/locale$/,// 匹配资源文件
        contextRegEsp: /moment$/, // 匹配检索目录
    })
]
1
2
3
4
5
6

# Cache

有些loader会有cache配置项,用来在编译代码后同时保持一份缓存,在执行下一次编译前会先检查源码文件是否有变化,如果没有就直接采用缓存。这样相当于实际编译只有变化了的文件,整体速度上回有一定的提升。

webpack5中有个新的配置项cache:{type:'filesystem'},它会在全局启用一个文件缓存。如果更新了一些相关配置,但是由于js源码没有发生变化,重新打包后还会是上一次的结果。 目前的解决方法是更新node_modules中的模块或webpack的配置后,手动修改cache.version来让缓存过期。

# 动态链接库与DllPlugin

对于第三方模块或者一些不常变化的模块,可以将它们预先编译和打包,然后项目实际构建过程中直接取用即可。

Code Splitting的思路是设置一些特定的规则并在打包过程中根据这些规则提取模块。DllPlugin是将vendor完全拆出来,在实际工程构建时就不用再对它进行任何处理,直接取用。理论上来说,DllPlugin比Code Splitting在打包速度上更胜一筹,但也增加了配置以及资源管理的复杂度。

# vendor配置

首先创建一个webpack配置文件,webpack.vendor.config.js

//webpack.vendor.config.js
const path = require('path');
const webpack = require('webpack');
const dllAssetPath = path.join(__dirname, 'dll');
const dllLibraryName = 'dllExample';
module.exports = {
    entry: ['react'],
    output: {
        path: dllAssetPath,
        filename: 'vendor.js',
        library: dllLibraryName
    },
    plugins: [
        new webpack.DllPlugin({
            name: dllLibararyName,
            path: path.join(dllAssetPath, 'manifest.json');
        })
    ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# vendor打包

// package.json
{
    "scripts": {
        "dll": "webpack --config webpack.vendor.config.js"
    }
}
1
2
3
4
5
6

执行后生成一个dll目录,里面有两个文件vendor.js和manifest.json。前者包含了库的代码,后者是资源清单。

# 链接到业务代码

使用与DllPlugin配套的插件DllReferencePlugin,它起到一个索引和链接的作用。通过DllReferencePlugin来获取刚刚打包好的资源清单,然后在页面中添加vendor.js的引用就可以了。

// webpack.config.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
    plugins: [
        new webpack.DllReferencePlugin({
            manifest: require(path.join(__dirname, 'dll/manifest.json'))
        })
    ]
}
1
2
3
4
5
6
7
8
9
10
<script src="dll/vendor.js"></script>
<script src="dist/app.js"></script>
1
2

当页面执行到vendor.js时,会声明dllExample全局变量。而manifest相当于我们注入app.js的资源地图,app.js会先通过name字段找到名为dllExample的library,再进一步获取其内部模块。

# 潜在问题

由于每个模块都有一个id,其值是按照数字顺序递增的。如果给vendor中增加一个其他模块。id会改变,可能会导致业务bundle.js。

// webpack.vendor.config.js
module.exports = {
    plugins: [
        new webpack.DllPlugin({}),
        new webpack.HashedModuleIdsPlugin()
    ]
}
1
2
3
4
5
6
7

# tree shaking

ES6 Module依赖关系的构建是在代码编译时而非运行时。基于这项特性webpack提供了tree shaking功能,它可以在打包过程中帮助我们检测工程中没有被引用过得模块,这部分代码将永远无法被执行到,因此也称为“死代码”。webpack会对这部分代码进行标记,并在资源压缩时将它们从最终的bundle中去掉。

tree shaking本身只是为死代码添加上标记,真正去除死代码是通过压缩工具进行的。